﻿#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////////////////////////
// MetaCollection
// 
// MetaCollection is a system to provide a standard structure, permissions and metadata service
// for content organization.
//
// This project is hosted at Google Code at http://code.google.com/p/metacollection/
//
// This project is licensed under the BSD license.  See the License.txt file for more information.
///////////////////////////////////////////////////////////////////////////////////////////////////
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using MetaCollection.Framework.Change;
using MetaCollection.Framework.Configuration;
using MetaCollection.Framework.Helpers;
using MetaCollection.Framework.Model;
using MetaCollection.Framework.Security;
using MetaCollection.Framework.Validation;

namespace MetaCollection.Framework.Provider
{
	/// <summary>
	/// The ResourceProvider is an implementation of the IProvider interface which 
	/// implements the RequestEventTypes.ResourceProviderEvents.  These events are used by the RequestHandler
	/// to implement the request processing pipeline for Item Requests.
	/// 
	/// As the implementation provider for the item request pipeline the ResourceProvider
	/// will serve as the business logic between the Request API <see cref="RequestHandler"/> and the
	/// persistence layer (Item and Resource Respositories).  The ResourceProvider is configurable
	/// so that different persistence stores can be used as well as different sets of validation rules.
	/// 
	/// 
	/// Example Scenario for Validation and Repository Config
	/// -----------------------------------------------------
	/// The validation rules provide custom Item Models by validating that the Item passing through
	/// the ResourceProvider meets a set of criteria.  For example a ResourceProvider could be configured
	/// to serve items that fit a NewsArticle Model.  In this example the NewsArticles may be stored in
	/// an existing NewsArticle database so the Item and Resource Repository implementations must be configured
	/// to connect to the existing database.  Because the existing database will have a 
	/// specific Model for a NewsArticle the ResourceProvider would be configured with validation to 
	/// match this existing set of business rules (required fields, field lengths) on item properties.
	/// 
	/// In addition to validating the presented item matches a certain model the Model contains properties
	/// which can be used to provide information to a client on what type of form to build to
	/// edit or create an item that will satisfy the model.
	/// 
	/// In this way when an Item is requested from the collection and is marked as processed by a 
	/// ResourceProvider+"NewsArticle config" it can be routed to the existing News database
	/// and the Item's properties can be validated against the business rules to ensure that the item
	/// is complete and correct.
	/// 
	/// -- Path Resolution
	/// When determining if a path presented to the application is correct it must be iteratively 
	/// evaluated by ResourceProviders for each element in the path.  The reason for this is easily
	/// seen with our NewsArticle example.  If a given article exists in the separate news article 
	/// database the only way to resolve a request to the article is to ask a 
	/// ResourceProvider+NewsArticleConfig to resolve the path segments against it's ItemRepository.
	/// 
	/// col://someitem1/someitem2/NewsItem(has ResourceProvider+NewsArticleConfig type)/article357
	/// 
	/// someItem2 will return the NewsItem record when asked for it.  This record indicates that
	/// the ResourceProvider+NewsArticleConfig should be used for any requests against it so that
	/// is what will be used to handle the request for article357.
	/// 
	/// It is this manner where an item knows the specification of the child items but requests against
	/// that item are handled by the child item's ProviderType that allows the entire MetaCollection
	/// to work.
	/// 
	/// </summary>
	public class ResourceProvider : IProvider
	{
		/// <summary>
		/// Unique identifier of the instance of the resource provider.
		/// </summary>
		private Guid m_Id = new Guid("579503E6-C89B-4590-95C2-08D7CEDF9E04");
		/// <summary>
		/// Friendly name of the resource type being provided
		/// </summary>
		private string m_Name;
		/// <summary>
		/// Description of the resource provider
		/// </summary>
		private string m_Description = "Provides an implementation of resource provider events that uses the standard Item and Resource repositories";
		/// <summary>
		/// Internal flag indicating if this provider instance has been initialzed.
		/// </summary>
		private bool m_Initialized;

		/// <summary>
		/// Initializes the provider specified in the ProviderType.
		/// </summary>
		/// <param name="providerConfig"></param>
		/// <exception cref="System.ArgumentNullException">The name of the provider is null.</exception>
		/// <exception cref="System.ArgumentException">The name of the provider has a length of zero</exception>
		/// <exception cref="System.InvalidOperationException"> An attempt is made to call 
		/// MetaCollection.Framework.Provider.IProvider.Initialize(MetaCollection.Framework.Model.IProviderType)
		/// on a provider after the provider has already been initialized</exception>
		public void Initialize(ProviderReferenceElement providerConfig)
		{
			lock (this)
			{
				if (m_Initialized)
					throw new InvalidOperationException("This provider has already been initialized.");
				m_Initialized = true;
			}
			if (providerConfig == null)
				throw new ArgumentNullException("providerConfig");

			if (String.IsNullOrEmpty(providerConfig.Name))
				throw new ArgumentException("Name of Provider can not be null", "providerConfig");
			m_Name = providerConfig.Name;
		}

		/// <summary>
		/// Ensures the provider has been initialized.
		/// </summary>
		/// <exception cref="InvalidOperationException">Thrown if <see cref="ResourceProvider.Initialize"/> has not been called.</exception>
		private void EnsureInitialized()
		{
			if (!m_Initialized)
				throw new InvalidOperationException("Resource Provider must be initialized prior to use.");
		}

		#region Public Properties
		/// <summary>
		/// Unique Identifier of this provider.
		/// </summary>
		/// <value></value>
		public Guid Id
		{
			get
			{
				EnsureInitialized();
				return m_Id;
			}
		}

		/// <summary>
		/// Gets the name of this provider.
		/// </summary>
		/// <value>The name.</value>
		public string Name
		{
			get
			{
				EnsureInitialized();
				return m_Name;
			}
		}
		/// <summary>
		/// Gets the friendly description of this provider.
		/// </summary>
		/// <value>The description.</value>
		public string Description
		{
			get
			{
				EnsureInitialized();
				return m_Description;
			}
		}

		/// <summary>
		/// Gets the Request Events that are supported by this provider type.
		/// </summary>
		/// <value>RequestEventTypes supported by this provider</value>
		public RequestEventTypes SupportedEvents
		{
			get { return RequestEventTypes.ResourceProviderEvents; }
		}
		#endregion

		#region IRequestSink Members (Not Implemented)

		public void OnRequestStart(object sender, EventArgs args)
		{
			throw new NotImplementedException();
		}

		public void OnBeforeValidateRequest(object sender, EventArgs args)
		{
			throw new NotImplementedException();
		}

		public void OnRequestValidateComplete(object sender, EventArgs args)
		{
			throw new NotImplementedException();
		}

		public void OnBeforePathResolve(object sender, EventArgs args)
		{
			throw new NotImplementedException();
		}

		public void OnPathResolveComplete(object sender, EventArgs args)
		{
			throw new NotImplementedException();
		}

		public void OnBeforeInvokeRequest(object sender, EventArgs args)
		{
			throw new NotImplementedException();
		}

		public void OnRequestComplete(object sender, System.EventArgs args)
		{
			throw new NotImplementedException();
		}

		#endregion

		#region IRequestSink Members (Implemented)

		public void OnHandlerDisposing(object sender, EventArgs args)
		{
		}

		/// <summary>
		/// Registered for calls by the <see cref="RequestHandler"/> during the validate request event.
		/// </summary>
		/// <param name="sender">The IRequestHandler sender.</param>
		/// <param name="args">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		public void OnValidateRequest(object sender, EventArgs args)
		{
			EnsureInitialized();

			ItemContext context = sender as ItemContext;
			if (context == null)
				throw new InvalidOperationException("The Current Request Context of this request can not be null.");

			//If no item is comming in, don't bother validating. 
			if (context.PostedItem == null)
			{
				context.IsValidRequest = true;
				return;
			}
			// try and load a validation specification model.  If there is not one then we assume that they do not wish to validate against a model.
			var spec = context.ModelRepository.FindSpecification(context.RequestedItem.Specification);
			if (spec == null)
			{
				context.IsValidRequest = true;
				return;
			}

			ItemValidation validator = new ItemValidation(spec);
			ValidationSession session = validator.StartSession();
			session.Validate("Name", context.PostedItem.Name);
			session.Validate("MimeType", context.PostedItem.MimeType);
			session.Validate("ExternalId", context.PostedItem.ExternalId);
			if (context.PostedItem.ParentId != null)
				session.Validate("ParentIdNotSelf", () => !context.PostedItem.Id.Equals(context.PostedItem.ParentId), "Parent Id cannot be set to the current Id");

			if (context.PostedItem.Version > 0)
			{
				session.Validate("LeftVersion", () => context.PostedItem.LeftVersion.HasValue ? (context.PostedItem.LeftVersion < context.PostedItem.Version) : true, "Version must be less than parent version");
				session.Validate("RightVersion", () => context.PostedItem.RightVersion.HasValue ? (context.PostedItem.RightVersion < context.PostedItem.Version) : true, "Version must be less than parent version");
			}

			// check each property against or model specification
			// there should not be a property with the same name as one of the base Item Model properties.  If there is this will throw an exception and that is OK.
			foreach (Model.IItemProperty prop in context.PostedItem.Properties)
				session.Validate(prop.KeyName, prop.KeyValue);

			try
			{
				session.VerifySession();
				context.IsValidRequest = true;
			}
			catch (InvalidOperationException ex)
			{
				context.ExecutionException = ex;
				context.IsValidRequest = false;
			}
		}

		/// <summary>
		/// Registered for calls by the <see cref="RequestHandler"/> during the provide path resolution event.
		/// </summary>
		/// <param name="sender">The IRequestHandler sender.</param>
		/// <param name="args">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		public void OnPathResolve(object sender, EventArgs args)
		{
			// If you can identify a provider, path resolution can be simplified from
			// col://foo/bar/baz/{SomeGuid}
			// to
			// col://{TheAppropriateProvider}/{SomeGuid}
			EnsureInitialized();

			ItemContext context = sender as ItemContext;
			if (context == null)
				throw new InvalidOperationException("The Current Request Context of this request can not be null.");

			if (context.RequestPathResolved.GetValueOrDefault(false))
				return;

			// take the next segment off of the context.RequestPath via context.CurrentPathSegment
			// perform a query on the context.ItemRepository looking for a child of context.Item
			// with a name matching the path segment.  Update RequestedItem and optionally set
			// pathresolution complete if this is the  last segment.  If the Item Repository does not
			// contain the specified item then it will return null which when passed to UpdatePathResolution()
			// will set context.RequestedItem to null indicating that the item does not exist or could not be resolved.		

			IItemInstance pathItem;
			//check here and if the segment matches a guid... pull by id not name.
			if (GuidHelper.IsGuid(context.CurrentPathSegment))
			{
				Guid id = new Guid(context.CurrentPathSegment);
				pathItem = context.ItemRepository.GetItem(id, LoadOptions.ItemReference).FirstOrDefault();
				pathItem = pathItem == null || context.RequestedItem.ParentId == pathItem.ParentId ? pathItem : null;
			}
			else // load an item matching the path segment name. only bother loading the permissions with the last segment since that is the only one we check.
				pathItem = context.ItemRepository.FindByName(context.RequestedItem.Id, context.CurrentPathSegment,
					(context.IsLastPathSegment ? LoadOptions.ItemReference : LoadOptions.ItemReference.Clear(LoadOptions.AccessClaims))).FirstOrDefault();
	
			if (pathItem != null && context.IsLastPathSegment)
				pathItem.DemandAccess(context.User, AccessRights.ReadProperties);

			context.UpdatePathResolution(pathItem, context.IsLastPathSegment);
		}

		/// <summary>
		/// Registered for calls by the <see cref="RequestHandler"/> during the provide item event.
		/// This method analyzes the request to see if the request requires an item(s) to be provided.
		/// If so then the item(s) is maniupulated according to the request.
		/// </summary>
		/// <param name="sender">The IRequestHandler sender.</param>
		/// <param name="args">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		public void OnProvideItem(object sender, EventArgs args)
		{
			#region setup and parameter validation
			EnsureInitialized();
			ItemContext context = sender as ItemContext;
			if (context == null)
				throw new InvalidOperationException("The Current Request Context of this request can not be null.");
			if (context.RequestType.HasAnyType(RequestTypes.GetResource | RequestTypes.SetResource))
				return;
			if (!context.RequestPathResolved.GetValueOrDefault(false))
				throw new InvalidOperationException("An Item can not be provided if the path to the item has not been resolved.");
			#endregion

			if (context.RequestType.HasType(RequestTypes.GetItem))
			{
				if (context.RequestType.HasAnyType(RequestTypes.ProcessDirectDescendents | RequestTypes.ProcessAllDescendents))
					context.ResponseItems = context.ItemRepository.FindByParent(context.RequestedItem.Id, context.RequestType.HasType(RequestTypes.ProcessAllDescendents), context.LoadOptions | LoadOptions.ItemReference).SanitizeForUserAccess(context.User);
				else
				{
					if (context.SpecificVersion.HasValue)
						context.ResponseItems = (new IItemInstance[] { context.ItemRepository.GetItem(context.RequestedItem.Id, context.SpecificVersion.Value, context.LoadOptions | LoadOptions.ItemReference) }).SanitizeForUserAccess(context.User);
					else if (context.RequestType.HasType(RequestTypes.VersionPublished))
						context.ResponseItems = context.ItemRepository.GetItem(context.RequestedItem.Id, context.LoadOptions | LoadOptions.ItemReference).Where(i => i.Published).SanitizeForUserAccess(context.User);
					else if (context.RequestType.HasType(RequestTypes.VersionAll))
						context.ResponseItems = context.ItemRepository.GetItem(context.RequestedItem.Id, context.LoadOptions | LoadOptions.ItemReference).SanitizeForUserAccess(context.User);
					else // latest version
						context.ResponseItems = (new IItemInstance[] { context.ItemRepository.GetItem(context.RequestedItem.Id, context.LoadOptions | LoadOptions.ItemReference).OrderByDescending(i => i.Version).FirstOrDefault() }).SanitizeForUserAccess(context.User);
				}
			}			
			else if (context.RequestType.HasType(RequestTypes.SetItem))
			{
				SaveOrUpdateItem(context);
			}
			else if (context.RequestType.HasType(RequestTypes.GetItemSpecification))
			{
				if (context.ModelRepository != null)
					context.ItemSpecification = context.ModelRepository.FindSpecification(context.RequestedItem.Specification);
				else
					context.ItemSpecification = null;
			}
			else if (context.RequestType.HasType(RequestTypes.DeleteItem))
			{
				DeleteItem(context);
			}
			// else not supported request type (PostToItem?)... do nothing.
		}

		/// <summary>
		/// Registered for calls by the <see cref="RequestHandler"/> during the provide resource event.
		/// This method analyzes the request to see if the request requires resource handling.  If so then
		/// the resource is maniupulated according to the request.
		/// </summary>
		/// <param name="sender">The IRequestHandler sender.</param>
		/// <param name="args">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		/// <exception cref="SecurityException">A security exception may be thrown if the CurrentContext user has
		/// insufficent rights to perform the request on the resource.</exception>
		public void OnProvideResource(object sender, EventArgs args)
		{
			#region setup and validation
			EnsureInitialized();

			ItemContext context = sender as ItemContext;
			if (context == null)
				throw new InvalidOperationException("The Current Request Context of this request can not be null.");

			if (!context.RequestType.HasAnyType(RequestTypes.GetResource | RequestTypes.SetResource))
				return;

			if (!context.RequestPathResolved.GetValueOrDefault(false))
				throw new InvalidOperationException("A Resource can not be provided if the path to the item has not been resolved.");

			if (context.SpecificVersion.HasValue)
				context.RequestedItem.Version = context.SpecificVersion.Value;
			#endregion

			if (context.RequestType.HasType(RequestTypes.GetResource))
			{
				context.RequestedItem.DemandAccess(context.User, AccessRights.ReadResource);
				context.ResponseResourceStream = context.ResourceRepository.GetResourceStream(context.RequestedItem);
			}
			else if (context.RequestType.HasType(RequestTypes.SetResource))
			{
				context.RequestedItem.DemandAccess(context.User, AccessRights.WriteResource);
				if ((context.PostedResource == null) || (!context.PostedResource.CanRead))
					throw new InvalidOperationException("Can not save resource since Posted Resource Stream is null or not readable.");
				context.ResourceRepository.SaveResourceStream(context.RequestedItem, context.PostedResource);
			}
		}

		#endregion

		/// <summary>
		/// Deletes the item as requested in the ItemContext.
		/// </summary>
		/// <param name="context">The context of the item request.</param>
		static void DeleteItem(ItemContext context)
		{
			context.RequestedItem.DemandAccess(context.User, AccessRights.DeleteItem);
			if (context.RequestType.HasType(RequestTypes.VersionSpecific))
				context.ItemRepository.RemoveItem(context.RequestedItem.Id, context.SpecificVersion.Value);
			else
				context.ItemRepository.RemoveItem(context.RequestedItem.Id);
		}

		/// <summary>
		/// Saves the update item as either a brand new item, changes to an existing version or a new version.
		/// </summary>
		/// <param name="context">The context of the item request.</param>
		static void SaveOrUpdateItem(ItemContext context)
		{
			IItemInstance postedItem = context.PostedItem;
			var currentItemSet = context.ItemRepository.GetItem(postedItem.Id, LoadOptions.ItemComplete);

			if (postedItem.Id == Guid.Empty || !currentItemSet.Any())
			{
				InsertItem(context);
			}
			else // updating an existing item (and optionally item children)
			{
				UpdateItem(context, currentItemSet);
			}
		}

		/// <summary>
		/// Inserts the item into the collection with a parent of context.RequestedItem. After insertion
		/// a query is immediately performed and the item+version(s) are returned.
		/// </summary>
		/// <param name="context">the item request context</param>
		/// <exception cref="InvalidOperationException">thrown if the request type is invalid.</exception>
		/// <exception cref="SecurityException">thrown if the user does not have permission to add this item to the parent given</exception>
		static void InsertItem(ItemContext context)
		{
			IItemInstance postedItem = context.PostedItem;

			if (context.RequestType.HasAnyType(RequestTypes.ProcessDirectDescendents | RequestTypes.ProcessAllDescendents))
				throw new InvalidOperationException("Set Item Request Can not be applied to Descendents when Item does not exist.");
			// make sure that the request meets the minimum load requirements
			if (!context.LoadOptions.IsSet(LoadOptions.ItemCreateReference))
				throw new InvalidOperationException("Creating a new item requires a minimum of the Item Create Reference load options to be set and provided.");

			// because this is an insert the path must point to the parent (location for insert) otherwise we would have failed with a not found already.
			postedItem.ParentId = context.RequestedItem.Id;
			postedItem.ItemClaimId = context.RequestedItem.ItemClaimId;
			context.RequestedItem.DemandAccess(context.User, AccessRights.Append);
			ItemChangeCollection changes =  new ItemChangeCollection(postedItem, null, context.LoadOptions);
			context.Changes = changes;
			//context.ResponseItems = context.ItemRepository.GetItem(postedItem.Id, context.LoadOptions).SanitizeForUserAccess(context.User);
			IItemInstance responseItem = context.ItemRepository.SaveItem(postedItem, changes); //, context.RequestType);
			if (responseItem == null)
				throw new InvalidOperationException("Save failed");
			context.ResponseItems = (new IItemInstance[] { context.ItemRepository.GetItem(responseItem.Id, responseItem.Version, LoadOptions.ItemComplete) }).SanitizeForUserAccess(context.User);
		}

		/// <summary>
		/// Updates the item.
		/// </summary>
		/// <param name="context">The context.</param>
		/// <param name="currentItemSet">The current item set.</param>
		static void UpdateItem(ItemContext context, IEnumerable<IItemInstance> currentItemSet)
		{
			IItemInstance postedItem = context.PostedItem;
			// existing item is the item with either the version given or the leftversion given (if version is null or not found).
			IItemInstance existingItem = currentItemSet.Where((i) => i.Version == postedItem.Version).FirstOrDefault() ?? currentItemSet.Where((i) => i.Version == postedItem.LeftVersion).FirstOrDefault();
			// if there is no version for left version then we are not deriving, if Version is not given then we are not updating so fail.
			if (existingItem == null)
				throw new InvalidOperationException("Invalid Version Number, Specified Version and Left Version are invalid.");
			postedItem.CreatedBy = context.User.Identity.Name; // make sure that the context of the user who is making these changes is in the createdby field.
			existingItem.Published = false; // because we are making a new version set the existingItem for computing changes to have a false published.  If they set published to true on their new version then this will require permission rights
			ItemChangeCollection changes = new ItemChangeCollection(postedItem, existingItem, context.LoadOptions);
			context.Changes = changes;
			existingItem.DemandAccess(context.User, changes.ChangeType.RequiredRightsOnItem());
			// Check to see if we need to do a permission check on the parent.
			if (changes.ChangeType.RequiredRightsOnParent() != AccessRights.None)
			{
				if (postedItem.ParentId == Guid.Empty)
					throw new NotImplementedException("The system does not currently have an implementation for adding new root items.  A check will need to be made for the System Administrator role on the user.");
				IItemInstance parent = context.ItemRepository.GetItem(postedItem.ParentId, context.LoadOptions).FirstOrDefault();
				if (parent == null)
					throw new InvalidOperationException("Parent Does not Exist In Resource Provider's Item Repository.");
				parent.DemandAccess(context.User, changes.ChangeType.RequiredRightsOnParent());
			}
			// if there are any non-permission changes including unknown changes being made to a published instance (as defined as the posted item has the same version as the published existing one) then fail.
			if (existingItem.Published && existingItem.Version == postedItem.Version && changes.ChangeType.HasAnyType(ItemChangeTypes.AllTypes ^ ItemChangeTypes.PermissionTypes))
				throw new InvalidOperationException("Cannot modify the published version of an item.");

			IItemInstance responseItem = context.ItemRepository.SaveItem(postedItem, changes); //, context.RequestType);
			if (context.RequestType.HasAnyType(RequestTypes.ProcessAllDescendents | RequestTypes.ProcessDirectDescendents))
			{
				IEnumerable<IItemInstance> children = context.ItemRepository.FindByParent(responseItem.Id, context.RequestType.HasType(RequestTypes.ProcessAllDescendents), LoadOptions.ItemReference | LoadOptions.Version);
				foreach (IItemInstance child in children)
				{
					int childVersion = child.Version; // catch the version number before changes are applied.
					changes.ApplyTo(child);  // apply the changes.
					child.LeftVersion = childVersion; // set the source version
					child.Version = 0; // mark to have a new version created.
					//context.ItemRepository.UpdateItemInstance(child, changes);
					context.ItemRepository.SaveItem(child, changes);
				}
			}

			context.ResponseItems = (new IItemInstance[] { context.ItemRepository.GetItem(responseItem.Id, responseItem.Version, context.LoadOptions | LoadOptions.ItemReference) }).SanitizeForUserAccess(context.User);
		}
	}

	internal static partial class Extensions
	{
		/// <summary>
		/// Sanitizes the specified items.
		/// </summary>
		/// <param name="items">The items.</param>
		/// <param name="user">The user to evaluate permissions for to determine how to sanitize the item.</param>
		/// <returns>a sanitized list of items</returns>
		internal static IEnumerable<IItemInstance> SanitizeForUserAccess(this IEnumerable<IItemInstance> items, MetaPrincipal userPermissions)
		{
			foreach (IItemInstance item in items)
			{
				if (item == null)
					continue;
				if (!item.HasAccess(userPermissions, AccessRights.ReadProperties))
					continue;
				if (!item.HasAccess(userPermissions, AccessRights.ReadItemClaims))
					item.AccessClaims = null; // clear out the access claims since the user can't read them.

				yield return item;
			}
		}
	}
}
